﻿using System;
using Mono.Cecil;
using Mono.Cecil.Cil;
using System.Collections.Generic;
using QuickGraph;
using QuickGraph.Algorithms.ConnectedComponents;

namespace CILADA.Engine.DynamicDependencyScanners
{
    public class AnalysisUtils
    {
        #region Constants
        const string STRING_TYPE = "System.String";
        #endregion

        public static ICollection<List<string>> CalculateCicularDependencies(List<AssemblyReference> assemblyReferences)
        {
            // build a graph of assemblies references.
            IDictionary<string, List<string>> depConnections = new Dictionary<string, List<String>>();

            assemblyReferences.Sort();

            foreach (AssemblyReference ar in assemblyReferences)
            {
                if (!depConnections.Keys.Contains(ar.From))
                {
                    depConnections.Add(ar.From, new List<string>());
                }

                if (!depConnections.Keys.Contains(ar.To))
                {
                    depConnections.Add(ar.To, new List<string>());
                }

                if (!depConnections[ar.From].Contains(ar.To))
                {
                    depConnections[ar.From].Add(ar.To);
                }
            }

            AdjacencyGraph<string, Edge<string>> graph = new AdjacencyGraph<string, Edge<string>>();
            string source = null;
            string target = null;
            foreach (KeyValuePair<string, List<string>> kvp in depConnections)
            {
                source = kvp.Key;
                if (!graph.ContainsVertex(source))
                {
                    graph.AddVertex(source);
                }
                foreach (string t in kvp.Value)
                {
                    target = t;
                    if (!graph.ContainsVertex(target))
                    {
                        graph.AddVertex(target);
                    }
                    graph.AddEdge(new Edge<string>(source, target));
                }
            }

            StronglyConnectedComponentsAlgorithm<string, Edge<string>> scca = new StronglyConnectedComponentsAlgorithm<string, Edge<string>>(graph);
            scca.Compute();

            // assuming that all those components that have the same value are strongly connected.
            IDictionary<int, List<string>> cycles = new Dictionary<int, List<string>>();

            int componentCount = scca.Components.Count;
            if (componentCount != 0)
            {
                foreach (KeyValuePair<string, int> kv in scca.Components)
                {
                    if (!cycles.Keys.Contains(kv.Value))
                    {
                        cycles.Add(kv.Value, new List<string>());
                    }
                    cycles[kv.Value].Add(kv.Key);
                }
            }

            return cycles.Values;
        }

        public static bool UsesParamsKeyword(MethodReference method)
        {
            if (method != null && method.Parameters.Count > 0)
            {
                // only need to check the last param, since that is the only one that can use the params keyword.
                ParameterDefinition p = method.Parameters[method.Parameters.Count - 1];
                string S_PAA = "System.ParamArrayAttribute";
                if (p.CustomAttributes.Count > 0)
                {
                    foreach (CustomAttribute ca in p.CustomAttributes)
                    {
                        if (S_PAA.Equals(ca.Constructor.DeclaringType.FullName))
                        {
                            return true;
                        }
                    }
                }
            }
            return false;
        }

        #region Methods for finding the literal value of string parameters.
        public static string FindValueOfIthStringParameter(MethodDefinition callingMethod, int instructionIndex, MethodReference methodToFindParamOf, int paramNumber)
        {
            //Console.WriteLine("Finding {0}th paramter for {1}. The method has parameters? {2} ", paramNumber, methodToFindParamOf.Name, methodToFindParamOf.HasParameters);

            // verify the method has enough parameters
            if (UsesParamsKeyword(methodToFindParamOf))
            {
                throw new ArgumentException("The method " + methodToFindParamOf.Name + " has an unknown number of parameters, as it uses the params keyword.");
            }
            if (methodToFindParamOf.Parameters.Count < paramNumber)
            {
                throw new ArgumentException("Cannot find the " + paramNumber + " in method " + methodToFindParamOf.Name + " as it only has " + methodToFindParamOf.Parameters.Count + " parameters");
            }

            // verify the instruction at the given index is a method call
            Instruction entryInstruction = callingMethod.Body.Instructions[instructionIndex];
            OpCode instrOpCode = entryInstruction.OpCode;
            if (!OpCodes.Call.Equals(instrOpCode) && !OpCodes.Calli.Equals(instrOpCode) && !OpCodes.Callvirt.Equals(instrOpCode))
            {
                throw new ArgumentException("The instruction OpCode at the specified index must be call, calli, or callvirt ");
            }

            // find the correct index to start tracing from.
            int paramSource = instructionIndex;

            // work out how many parameters to go backwards
            // hence, need to work out how many string parameters the method has
            int numStringParams = NumStringParamsOfMethod(methodToFindParamOf);
            int offset = NumStringParamsBeforeParamNumber(paramNumber, methodToFindParamOf);
            int numberToBackTrack = (numStringParams - offset);

            // now find the value of the given instruction, or if that isnt possible, trace the variable back as far as possible.
            paramSource = GetStringParameterSourceInstruction(callingMethod, numberToBackTrack, paramSource);

            string result = "not found";
            if (-1 != paramSource)
            {
                Instruction sourceInstr = callingMethod.Body.Instructions[paramSource];
                result = (null != sourceInstr.Operand) ? sourceInstr.Operand.ToString() : "no operand found...";
            }
            return result;
        }

        public static int GetStringParameterSourceInstruction(MethodDefinition method, int numberOfStringsToBackTrack, int instructionIndex)
        {
            if (instructionIndex.Equals(0))
            {
                throw new ArgumentException("Cannot search backwards from an instruction index of 0.");
            }

            bool foundParamValue = false;
            Instruction instruction = method.Body.Instructions[instructionIndex];

            while (!foundParamValue)
            {
                // go to previous instruction
                if (numberOfStringsToBackTrack < 1)
                {
                    // cant find string, as have backtracked too far without finding something useful.
                    break;
                }
                else if (null != instruction.Previous && !instructionIndex.Equals(0))
                {
                    instruction = instruction.Previous;
                    instructionIndex--;
                }
                else
                {
                    string errorMessage = "Could not find the source of the value in method " + method.DeclaringType.FullName + "::" + method.Name;
                    break;
                }

                #region String Producers

                // look for ldstr
                if (OpCodes.Ldstr.Equals(instruction.OpCode))
                {
                    // find the string that was being loaded.
                    if (instruction.Operand is string)
                    {
                        //Console.WriteLine("> Found string '" + (instruction.Operand as string) + "' (bN="+numberOfStringsToBackTrack+")");
                        if (numberOfStringsToBackTrack.Equals(1))
                        {
                            return instructionIndex;
                        }
                        else
                        {
                            numberOfStringsToBackTrack--;
                            continue;
                        }
                    }
                }

                // some opcodes to look out for.
                // cant use a switch here as opcodes are not constants.
                if (OpCodes.Ldelem_Any.Equals(instruction.OpCode))
                {
                    // get the type and see if it is what we are looking for
                    if (instruction.Operand is string)
                    {
                        string ldelemValue = instruction.Operand as string;
                        //Console.WriteLine("> Found Ldemlem value='" + ldelemValue + "'");
                        if (numberOfStringsToBackTrack.Equals(1))
                        {
                            return instructionIndex;
                        }
                        else
                        {
                            numberOfStringsToBackTrack--;
                            continue;
                        }
                    }
                }
                // check to see if a parameter is being loaded
                else if (instruction.OpCode.Name.StartsWith("ldarg"))
                {
                    // TODO: fix this to handle the cases for ldarg.[0..3], where there is no operand.

                    // get the type and see if it is what we are looking for
                    if (instruction.Operand is string)
                    {
                        string ldargValue = instruction.Operand as string;
                        //Console.WriteLine("> Found Ldarg value='" + ldargValue + "'");
                        if (numberOfStringsToBackTrack.Equals(1))
                        {
                            return instructionIndex;
                        }
                        else
                        {
                            numberOfStringsToBackTrack--;
                            continue;
                        }
                    }
                }
                // check to see if an object of the desired type was created in the method call.
                else if (OpCodes.Newobj.Equals(instruction.OpCode))
                {
                    var methodReference = instruction.Operand as MethodReference;
                    if (methodReference != null)
                    {
                        string returnType = methodReference.ReturnType.ReturnType.FullName;
                        if (STRING_TYPE.Equals(returnType))
                        {
                            // have a new object of the correct type being loaded
                            //Console.WriteLine("> Found Newobj from " + methodReference.ToString());
                            if (numberOfStringsToBackTrack.Equals(1))
                            {
                                return instructionIndex;
                            }
                            else
                            {
                                numberOfStringsToBackTrack--;
                                // increase backtrack number by number of string params the ctor uses.
                                numberOfStringsToBackTrack += NumStringParamsOfMethod(methodReference);
                                continue;
                            }
                        }
                    }
                }
                // check for methods
                else if (OpCodes.Call.Equals(instruction.OpCode) || OpCodes.Calli.Equals(instruction.OpCode)
                    || OpCodes.Callvirt.Equals(instruction.OpCode))
                {
                    var methodReference = instruction.Operand as MethodReference;
                    if (methodReference != null)
                    {
                        int numStringParams = NumStringParamsOfMethod(methodReference);
                        numberOfStringsToBackTrack += numStringParams;
                    }
                }
                // check to see if a variable is being loaded.
                else if (instruction.OpCode.Name.StartsWith("ldloc"))
                {
                    // TODO: fix this to handle the cases for ldloc.[0..3], where there is no operand.

                    // get the type and see if it is what we are looking for
                    if (instruction.Operand is string)
                    {
                        string ldlocValue = instruction.Operand as string;
                        //Console.WriteLine("> Found Ldloc value='" + ldlocValue + "'");
                        if (numberOfStringsToBackTrack.Equals(1))
                        {
                            return instructionIndex;
                        }
                        else
                        {
                            numberOfStringsToBackTrack--;
                            continue;
                        }
                    }
                }
                // check for cast calls with the correct result type
                else if (OpCodes.Castclass.Equals(instruction.OpCode))
                {
                    if (instruction.Operand is Type)
                    {
                        Type type = (instruction.Operand as Type);
                        if (type.FullName.Equals(STRING_TYPE))
                        {
                            //Console.WriteLine("> Found cast to type '" + type.FullName + "'");
                            if (numberOfStringsToBackTrack.Equals(1))
                            {
                                return instructionIndex;
                            }
                            else
                            {
                                numberOfStringsToBackTrack--;
                                continue;
                            }
                        }
                    }
                }
                // check for loading of a static field, ldsfld
                else if (OpCodes.Ldsfld.Equals(instruction.OpCode))
                {
                    if (instruction.Operand is string)
                    {
                        string ldsfldValue = instruction.Operand as string;
                        //Console.WriteLine("> Found ldsfld '" + ldsfldValue + "'");
                        if (numberOfStringsToBackTrack.Equals(1))
                        {
                            return instructionIndex;
                        }
                        else
                        {
                            numberOfStringsToBackTrack--;
                            continue;
                        }
                    }
                }
                // check for loading on a field, ldfld
                else if (OpCodes.Ldfld.Equals(instruction.OpCode))
                {
                    if (instruction.Operand is string)
                    {
                        string ldfldValue = instruction.Operand as string;
                        //Console.WriteLine("> Found ldfld '" + ldfldValue + "'");
                        if (numberOfStringsToBackTrack.Equals(1))
                        {
                            return instructionIndex;
                        }
                        else
                        {
                            numberOfStringsToBackTrack--;
                            continue;
                        }
                    }
                }
                #endregion
                #region
                //else if (instruction.OpCode.Name.StartsWith("stloc"))
                //{
                //    int varIndex = -1;
                //    if (instruction.OpCode.Equals(OpCodes.Stloc) || instruction.OpCode.Equals(OpCodes.Stloc_S))
                //    {
                //        try
                //        {
                //            varIndex = Convert.ToInt32(instruction.Operand);
                //        }
                //        catch (InvalidCastException)
                //        {
                //            return -1;
                //        }
                //    }
                //    else if (instruction.OpCode.Equals(OpCodes.Stloc_0))
                //    {
                //        varIndex = 0;
                //    }
                //    else if (instruction.OpCode.Equals(OpCodes.Stloc_1))
                //    {
                //        varIndex = 1;
                //    }
                //    else if (instruction.OpCode.Equals(OpCodes.Stloc_2))
                //    {
                //        varIndex = 2;
                //    }
                //    else if (instruction.OpCode.Equals(OpCodes.Stloc_3))
                //    {
                //        varIndex = 3;
                //    }

                //    VariableDefinition var = method.Body.Variables[varIndex];
                //    if (var.VariableType.FullName.Equals(STRING_TYPE))
                //    {
                //        numberOfStringsToBackTrack++;
                //        continue;
                //    }
                //}
                else if (OpCodes.Stobj.Equals(instruction.OpCode))
                {
                    Type t = instruction.Operand as Type;
                    if (t.FullName.Equals(STRING_TYPE))
                    {
                        numberOfStringsToBackTrack++;
                        continue;
                    }
                }
                else if (instruction.OpCode.Name.StartsWith("stelem"))
                {
                    try
                    {
                        Type t = instruction.Operand as Type;
                        if (t.FullName.Equals(STRING_TYPE))
                        {
                            numberOfStringsToBackTrack++;
                            continue;
                        }
                    }
                    catch (NullReferenceException)
                    {
                        return -1;
                    }
                }
                else if (OpCodes.Stfld.Equals(instruction.OpCode))
                {
                    FieldReference fr = instruction.Operand as FieldReference;
                    if (fr.FieldType.FullName.Equals(STRING_TYPE))
                    {
                        numberOfStringsToBackTrack++;
                        continue;
                    }
                }
                else if (OpCodes.Stsfld.Equals(instruction.OpCode))
                {
                    FieldReference fr = instruction.Operand as FieldReference;
                    if (fr.FieldType.FullName.Equals(STRING_TYPE))
                    {
                        numberOfStringsToBackTrack++;
                        continue;
                    }
                }
                #endregion
            }

            return -1;
        }

        private static int NumStringParamsOfMethod(MethodReference method)
        {
            int count = 0;
            foreach (ParameterDefinition pd in method.Parameters)
            {
                if (pd.ParameterType.FullName.Equals("System.String"))
                {
                    count++;
                }
            }
            return count;
        }

        private static int NumStringParamsBeforeParamNumber(int paramNumber, MethodReference method)
        {
            int countTotalParams = 0;
            int countStringParams = 0;
            foreach (ParameterDefinition pd in method.Parameters)
            {
                countTotalParams++;

                if (countTotalParams >= paramNumber)
                {
                    break;
                }

                if (pd.ParameterType.FullName.Equals("System.String"))
                {
                    countStringParams++;
                }
            }
            return countStringParams;
        }

        private static int NumStringParamsAfterParamNumber(int paramNumber, MethodReference method)
        {
            int countTotalParams = 0;
            int countStringParams = 0;
            foreach (ParameterDefinition pd in method.Parameters)
            {
                countTotalParams++;

                if (countTotalParams <= paramNumber)
                {
                    continue;
                }

                if (pd.ParameterType.FullName.Equals("System.String"))
                {
                    countStringParams++;
                }
            }
            return countStringParams;
        }
        #endregion
    }
}
